// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

import 'package:pub_semver/pub_semver.dart';

import '../exceptions.dart';
import '../language_version.dart';
import '../package_name.dart';
import '../pubspec.dart';
import '../sdk.dart';
import '../source.dart';
import '../system_cache.dart';
import '../utils.dart';

/// A package [Source] that gets packages from a hard-coded SDK.
class SdkSource extends Source {
  static final SdkSource instance = SdkSource._();

  SdkSource._();

  @override
  final name = 'sdk';

  /// Parses an SDK dependency.
  @override
  PackageRef parseRef(
    String name,
    Object? description, {
    required Description containingDescription,
    LanguageVersion? languageVersion,
  }) {
    if (description is! String) {
      throw FormatException('The description must be an SDK name.');
    }

    return PackageRef(name, SdkDescription(description));
  }

  @override
  PackageId parseId(
    String name,
    Version version,
    Object? description, {
    String? containingDir,
  }) {
    if (description is! String) {
      throw FormatException('The description must be an SDK name.');
    }

    return PackageId(
      name,
      version,
      ResolvedSdkDescription(SdkDescription(description)),
    );
  }

  @override
  Future<List<PackageId>> doGetVersions(
    PackageRef ref,
    Duration? maxAge,
    SystemCache cache,
  ) async {
    final description = ref.description;
    if (description is! SdkDescription) {
      throw ArgumentError('Wrong source');
    }
    var pubspec = _loadPubspec(ref, cache);
    var id = PackageId(
      ref.name,
      pubspec.version,
      ResolvedSdkDescription(description),
    );
    // Store the pubspec in memory if we need to refer to it again.
    cache.cachedPubspecs[id] = pubspec;
    return [id];
  }

  @override
  Future<Pubspec> doDescribe(
    PackageId id,
    SystemCache cache,
  ) async =>
      _loadPubspec(id.toRef(), cache);

  /// Loads the pubspec for the SDK package named [ref].
  ///
  /// Throws a [PackageNotFoundException] if [ref]'s SDK is unavailable or
  /// doesn't contain the package.
  Pubspec _loadPubspec(PackageRef ref, SystemCache cache) {
    var pubspec = Pubspec.load(
      _verifiedPackagePath(ref),
      cache.sources,
      expectedName: ref.name,
      containingDescription: ref.description,
    );

    /// Validate that there are no non-sdk dependencies if the SDK does not
    /// allow them.
    if (ref.description case SdkDescription description) {
      if (sdks[description.sdk]
          case Sdk(allowsNonSdkDepsInSdkPackages: false)) {
        for (var dep in pubspec.dependencies.entries) {
          if (dep.value.source is! SdkSource) {
            throw UnsupportedError(
              'Only SDK packages are allowed as regular dependencies for '
              'packages vendored by the ${sdk.identifier} SDK, but the '
              '`${ref.name}` package has a ${dep.value.source.name} dependency '
              'on `${dep.key}`.',
            );
          }
        }
      }
    }
    return pubspec;
  }

  /// Returns the path for the given [ref].
  ///
  /// Throws a [PackageNotFoundException] if [ref]'s SDK is unavailable or
  /// doesn't contain the package.
  String _verifiedPackagePath(PackageRef ref) {
    final description = ref.description;
    if (description is! SdkDescription) {
      throw ArgumentError('Wrong source');
    }
    var sdkName = description.sdk;
    var sdk = sdks[sdkName];
    if (sdk == null) {
      throw PackageNotFoundException('unknown SDK "$sdkName"');
    } else if (!sdk.isAvailable) {
      throw PackageNotFoundException(
        'the ${sdk.name} SDK is not available',
        hint: sdk.installMessage,
      );
    }

    var path = sdk.packagePath(ref.name);
    if (path != null) return path;

    throw PackageNotFoundException(
      'could not find package ${ref.name} in the ${sdk.name} SDK',
    );
  }

  @override
  String doGetDirectory(
    PackageId id,
    SystemCache cache, {
    String? relativeFrom,
  }) {
    try {
      return _verifiedPackagePath(id.toRef());
    } on PackageNotFoundException catch (error) {
      // [PackageNotFoundException]s are uncapitalized and unpunctuated because
      // they're used within other sentences by the version solver, but
      // [ApplicationException]s should be full sentences.
      throw ApplicationException('${capitalize(error.message)}.');
    }
  }
}

class SdkDescription extends Description {
  /// The sdk the described package comes from.
  final String sdk;

  SdkDescription(this.sdk);
  @override
  String format() => sdk;

  @override
  Object? serializeForPubspec({
    required String? containingDir,
    required LanguageVersion languageVersion,
  }) {
    return sdk;
  }

  @override
  Source get source => SdkSource.instance;

  @override
  int get hashCode => sdk.hashCode;

  @override
  bool operator ==(Object other) {
    return other is SdkDescription && other.sdk == sdk;
  }
}

class ResolvedSdkDescription extends ResolvedDescription {
  @override
  SdkDescription get description => super.description as SdkDescription;

  ResolvedSdkDescription(SdkDescription super.description);

  @override
  Object? serializeForLockfile({required String? containingDir}) {
    return description.sdk;
  }

  @override
  int get hashCode => description.hashCode;

  @override
  bool operator ==(Object other) {
    return other is ResolvedSdkDescription && other.description == description;
  }
}
